/******************************************************************************* * Copyright (c) 2003, 2016 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.ui.progress; import org.eclipse.core.runtime.Adapters; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.ListenerList; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.IJobChangeListener; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.jobs.JobChangeAdapter; import org.eclipse.jface.util.Util; import org.eclipse.jface.viewers.AbstractTreeViewer; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.widgets.Control; import org.eclipse.ui.IWorkbenchPartSite; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.internal.progress.ProgressMessages; import org.eclipse.ui.model.IWorkbenchAdapter; /** * The DeferredContentManager is a class that helps an ITreeContentProvider get * its deferred input. * * <b>NOTE</b> AbstractTreeViewer#isExpandable may need to be implemented in * AbstractTreeViewer subclasses with deferred content that use filtering as a * call to #getChildren may be required to determine the correct state of the * expanding control. * * AbstractTreeViewers which use this class may wish to sacrifice accuracy of * the expandable state indicator for the performance benefits of deferring * content. * * @see IDeferredWorkbenchAdapter * @since 3.0 */ public class DeferredTreeContentManager { AbstractTreeViewer treeViewer; IWorkbenchSiteProgressService progressService; private ListenerList<IJobChangeListener> updateCompleteListenerList; /** * The DeferredContentFamily is a class used to keep track of a * manager-object pair so that only jobs scheduled by the receiver are * canceled by the receiver. * * @since 3.1 * */ class DeferredContentFamily { protected DeferredTreeContentManager manager; protected Object element; /** * Create a new instance of the receiver to define a family for object * in a particular scheduling manager. * * @param schedulingManager * @param object */ DeferredContentFamily(DeferredTreeContentManager schedulingManager, Object object) { this.manager = schedulingManager; this.element = object; } } /** * Create a new instance of the receiver using the supplied content provider * and viewer. Run any jobs using the site. * * @param provider * @param viewer * @param site * @deprecated in 3.4. provider is not used by this class */ @Deprecated public DeferredTreeContentManager(ITreeContentProvider provider, AbstractTreeViewer viewer, IWorkbenchPartSite site) { this(viewer, site); } /** * Create a new instance of the receiver using the supplied content provider * and viewer. * * @param provider * The content provider that will be updated * @param viewer * The tree viewer that the results are added to * @deprecated in 3.4. provider is not used by this class */ @Deprecated public DeferredTreeContentManager(ITreeContentProvider provider, AbstractTreeViewer viewer) { this(viewer); } /** * Create a new instance of the receiver using the supplied content provider * and viewer. Run any jobs using the site. * * @param viewer * @param site * * @since 3.4 */ public DeferredTreeContentManager(AbstractTreeViewer viewer, IWorkbenchPartSite site) { this(viewer); Object siteService = Adapters.adapt(site, IWorkbenchSiteProgressService.class); if (siteService != null) { progressService = (IWorkbenchSiteProgressService) siteService; } } /** * Create a new instance of the receiver using the supplied content provider * and viewer. * * @param viewer * The tree viewer that the results are added to * * @since 3.4 */ public DeferredTreeContentManager(AbstractTreeViewer viewer) { treeViewer = viewer; } /** * Provides an optimized lookup for determining if an element has children. * This is required because elements that are populated lazilly can't answer * <code>getChildren</code> just to determine the potential for children. * Throw an AssertionFailedException if element is null. * * @param element * The Object being tested. This should not be <code>null</code>. * @return boolean <code>true</code> if there are potentially children. * @throws RuntimeException * if the element is null. */ public boolean mayHaveChildren(Object element) { Assert.isNotNull(element, ProgressMessages.DeferredTreeContentManager_NotDeferred); IDeferredWorkbenchAdapter adapter = getAdapter(element); return adapter != null && adapter.isContainer(); } /** * Returns the child elements of the given element, or in the case of a * deferred element, returns a placeholder. If a deferred element is used, a * job is created to fetch the children in the background. * * @param parent * The parent object. * @return Object[] or <code>null</code> if parent is not an instance of * IDeferredWorkbenchAdapter. */ public Object[] getChildren(final Object parent) { IDeferredWorkbenchAdapter element = getAdapter(parent); if (element == null) { return null; } PendingUpdateAdapter placeholder = createPendingUpdateAdapter(); startFetchingDeferredChildren(parent, element, placeholder); return new Object[] { placeholder }; } /** * Factory method for creating the pending update adapter representing the * placeholder node. Subclasses may override. * * @return a pending update adapter * @since 3.2 */ protected PendingUpdateAdapter createPendingUpdateAdapter() { return new PendingUpdateAdapter(); } /** * Return the IDeferredWorkbenchAdapter for element or the element if it is * an instance of IDeferredWorkbenchAdapter. If it does not exist return * null. * * @param element * @return IDeferredWorkbenchAdapter or <code>null</code> */ protected IDeferredWorkbenchAdapter getAdapter(Object element) { return Adapters.adapt(element, IDeferredWorkbenchAdapter.class); } /** * Starts a job and creates a collector for fetching the children of this * deferred adapter. If children are waiting to be retrieved for this parent * already, that job is cancelled and another is started. * * @param parent * The parent object being filled in, * @param adapter * The adapter being used to fetch the children. * @param placeholder * The adapter that will be used to indicate that results are * pending. */ protected void startFetchingDeferredChildren(final Object parent, final IDeferredWorkbenchAdapter adapter, final PendingUpdateAdapter placeholder) { final IElementCollector collector = createElementCollector(parent, placeholder); // Cancel any jobs currently fetching children for the same parent // instance. cancel(parent); String jobName = getFetchJobName(parent, adapter); Job job = new Job(jobName) { @Override public IStatus run(IProgressMonitor monitor) { adapter.fetchDeferredChildren(parent, collector, monitor); if (monitor.isCanceled()) { return Status.CANCEL_STATUS; } return Status.OK_STATUS; } @Override public boolean belongsTo(Object family) { if (family instanceof DeferredContentFamily) { DeferredContentFamily contentFamily = (DeferredContentFamily) family; if (contentFamily.manager == DeferredTreeContentManager.this) { return isParent(contentFamily, parent); } } return false; } /** * Check if the parent of element is equal to the parent used in * this job. * * @param family * The DeferredContentFamily that defines a potential * ancestor of the current parent in a particular * manager. * @param child * The object to check against. * @return boolean <code>true</code> if the child or one of its * parents are the same as the element of the family. */ private boolean isParent(DeferredContentFamily family, Object child) { if (family.element.equals(child)) { return true; } IWorkbenchAdapter workbenchAdapter = getWorkbenchAdapter(child); if (workbenchAdapter == null) { return false; } Object elementParent = workbenchAdapter.getParent(child); if (elementParent == null) { return false; } return isParent(family, elementParent); } /** * Get the workbench adapter for the element. * * @param element * The object we are adapting to. */ private IWorkbenchAdapter getWorkbenchAdapter(Object element) { return Adapters.adapt(element, IWorkbenchAdapter.class); } }; job.addJobChangeListener(new JobChangeAdapter() { @Override public void done(IJobChangeEvent event) { runClearPlaceholderJob(placeholder); } }); job.setRule(adapter.getRule(parent)); if (progressService == null) { job.schedule(); } else { progressService.schedule(job); } } /** * Returns a name to use for the job that fetches children of the given * parent. Subclasses may override. Default job name is parent's label. * * @param parent * parent that children are to be fetched for * @param adapter * parent's deferred adapter * @return job name */ protected String getFetchJobName(Object parent, IDeferredWorkbenchAdapter adapter) { return NLS.bind( ProgressMessages.DeferredTreeContentManager_FetchingName, adapter.getLabel(parent)); } /** * Create a UIJob to add the children to the parent in the tree viewer. * * @param parent * @param children * @param monitor */ protected void addChildren(final Object parent, final Object[] children, IProgressMonitor monitor) { WorkbenchJob updateJob = new WorkbenchJob( ProgressMessages.DeferredTreeContentManager_AddingChildren) { @Override public IStatus runInUIThread(IProgressMonitor updateMonitor) { // Cancel the job if the tree viewer got closed if (treeViewer.getControl().isDisposed() || updateMonitor.isCanceled()) { return Status.CANCEL_STATUS; } treeViewer.add(parent, children); return Status.OK_STATUS; } }; updateJob.setSystem(true); updateJob.schedule(); } /** * Return whether or not the element is or adapts to an * IDeferredWorkbenchAdapter. * * @param element * @return boolean <code>true</code> if the element is an * IDeferredWorkbenchAdapter */ public boolean isDeferredAdapter(Object element) { return getAdapter(element) != null; } /** * Run a job to clear the placeholder. This is used when the update for the * tree is complete so that the user is aware that no more updates are * pending. * * @param placeholder */ protected void runClearPlaceholderJob(final PendingUpdateAdapter placeholder) { if (placeholder.isRemoved() || !PlatformUI.isWorkbenchRunning()) { return; } // Clear the placeholder if it is still there WorkbenchJob clearJob = new WorkbenchJob( ProgressMessages.DeferredTreeContentManager_ClearJob) { @Override public IStatus runInUIThread(IProgressMonitor monitor) { if (!placeholder.isRemoved()) { Control control = treeViewer.getControl(); if (control.isDisposed()) { return Status.CANCEL_STATUS; } treeViewer.remove(placeholder); placeholder.setRemoved(true); } return Status.OK_STATUS; } }; clearJob.setSystem(true); if (updateCompleteListenerList != null) { for (IJobChangeListener listener : updateCompleteListenerList) { clearJob.addJobChangeListener(listener); } } // See bug 470554 if IElementCollector.done() is called immediately // after IElementCollector.add(), SWT/GTK seem to be confused. // Delay tree element deletion to avoid race conditions with GTK code long timeout = Util.isGtk() ? 100 : 0; clearJob.schedule(timeout); } /** * Cancel all jobs that are fetching content for the given parent or any of * its children. * * @param parent */ public void cancel(Object parent) { if (parent == null) { return; } Job.getJobManager().cancel(new DeferredContentFamily(this, parent)); } /** * Create the element collector for the receiver. * * @param parent * The parent object being filled in, * @param placeholder * The adapter that will be used to indicate that results are * pending. * @return IElementCollector */ protected IElementCollector createElementCollector(final Object parent, final PendingUpdateAdapter placeholder) { return new IElementCollector() { @Override public void add(Object element, IProgressMonitor monitor) { add(new Object[] { element }, monitor); } @Override public void add(Object[] elements, IProgressMonitor monitor) { addChildren(parent, elements, monitor); } @Override public void done() { runClearPlaceholderJob(placeholder); } }; } /** * Add a listener to list of update complete listeners. These listeners are * attached to the job that updates the viewer content (clears the pending * entry, etc.) after all deferred content has been retrieved. * * This method has no effect if the listener has already been added to the * list of listeners. * * Since 3.6, this listener is added to a list of listeners rather than * replacing the previously added listener. For backward compatibility, * adding a null listener will be interpreted as removal of a listener if * only one listener has been registered. * * @param listener * the listener to add to the list of update listeners * @since 3.4 */ public void addUpdateCompleteListener(IJobChangeListener listener){ // Maintain backward compatibility. // Earlier only one listener was supported, so it can be removed by // passing null if (listener == null && updateCompleteListenerList != null) { Object[] listeners = updateCompleteListenerList.getListeners(); if (listeners.length == 1) { removeUpdateCompleteListener((IJobChangeListener) listeners[0]); } } else { if (updateCompleteListenerList == null) { updateCompleteListenerList = new ListenerList<>(); } updateCompleteListenerList.add(listener); } } /** * Removes the listener from the list of update listeners that are attached * to the job that updates the viewer content (clears the pending entry, * etc.) after all deferred content has been retrieved. If the listener is * already attached to a running job, it is not removed, but it will not be * added to any subsequent jobs that are run. * * This method has no effect if the listener was not previously added to the * listener list. * * @param listener * the listener to be removed * @since 3.6 */ public void removeUpdateCompleteListener(IJobChangeListener listener) { if (updateCompleteListenerList != null) { updateCompleteListenerList.remove(listener); } } }